---
title: Architecture
summary: Internal structure, JsonHttpClient wrapping, and construction of PdfAPI.Client.
status: draft
owner: SAIF Doc Gen
last_reviewed: 2026-09-17
---

# Architecture

`PdfClient` is a single, immutable-after-construction class that implements `IPdfClient`, wraps one `SAIF.Web.API.Clients.JsonHttpClient` field for its five remote members, and delegates its six local members to the static `PdfAPI.Core.PdfClientService`.

## Architecture diagram

```mermaid
flowchart TD
    CONSUMER[Consuming application] --> IFACE["IPdfClient interface"]
    IFACE --> IMPL["PdfClient : IPdfClient"]
    IMPL --> INFO["ClientInfo<br/>caller-supplied, not populated by PdfClient"]
    IMPL --> HTTP["SAIF.Web.API.Clients.JsonHttpClient"]
    IMPL --> LOCAL["PdfAPI.Core.PdfClientService<br/>6 non-HTTP members"]
    HTTP --> URI["baseUri = url.TrimEnd('/') + '/v1/'"]
    URI --> API["PdfAPI REST API"]
    IMPL --> CONTRACTS["PdfAPI.Core contracts"]
    API --> CONTRACTS
```

## Component responsibilities

| Component | Type | Responsibility |
| --- | --- | --- |
| `IPdfClient` | Interface | The public contract; declares 9 of the client's 11 public members |
| `PdfClient` | Class | Composes the relative URL per remote member, delegates to `JsonHttpClient`, and forwards local members to `PdfClientService`; also exposes `IsPdfa` and `Track`, which are on the concrete class only |
| `JsonHttpClient` | `SAIF.Web.API.Clients` | Serialization, transport, timeout, and the `PostJson`/`GetResult` result envelope |
| `ClientInfo` | `SAIF.Web.API.Clients` | Caller identity sent with each request; supplied by the caller, not built by `PdfClient` |
| `PdfAPI.Core` | Contracts assembly | Request and response DTOs shared with the API (`ConvertRequest`, `MergeRequest`, `PdfResult`, `PdfTrackingInfo`, `PdfFormat`, `DocumentRange`) |
| `PdfClientService` | Local service (`PdfAPI.Core.Services`) | Backs the six non-HTTP members: `Version`, `Pages`, `IsPdf`, `HasFormFields`, `Flatten`, `Extract` |

## Construction

`PdfClient` has exactly one public constructor and no separate initializer. There is no parameterless constructor and no two-phase initializer method — the client cannot be constructed in an uninitialized state, unlike a client with a separate initializer method taking a URL and timeout.

| Constructor / initializer | Parameters | Effect |
| --- | --- | --- |
| `PdfClient(ClientInfo info, string url)` | `info` — caller-supplied identity; `url` — API base URL | Builds `baseUri` from `url` and constructs the `JsonHttpClient` field, which is `readonly` |
| Parameterless constructor | — | Not present |
| Two-phase initializer method | — | Not present |

### Base URI composition

```csharp
var baseUri = new Uri($"{url.TrimEnd('/')}/v1/");
_jsonHttpClient = new JsonHttpClient(baseUri, info);
```

| Element | Value | Source |
| --- | --- | --- |
| Base URL | Caller-supplied `url` constructor argument | `PdfAPI.Client\PdfClient.cs:15` |
| Version suffix | `v1/` | Hard-coded in `PdfClient.cs:17` |
| Trailing-slash handling | `url.TrimEnd('/')` applied before appending `/v1/` | `PdfAPI.Client\PdfClient.cs:17` |

Both `http://host/pdfapi` and `http://host/pdfapi/` compose the same base URI. There is no guard on `url` itself: a `null` value throws `NullReferenceException` from `TrimEnd`, and an empty/relative value throws `UriFormatException` from the `Uri` constructor.

### ClientInfo population

| Property | Value | Source |
| --- | --- | --- |
| `ApplicationId` | Not set by `PdfClient` | Caller-supplied `info` argument, used as-is |
| `MachineName` | Not set by `PdfClient` | Caller-supplied `info` argument, used as-is |
| `UserId` | Not set by `PdfClient` | Caller-supplied `info` argument, used as-is |

Every consumer observed in scope passes a default-constructed, empty `ClientInfo` (`PdfApiClientTester\PdfApiClientTester\PdfTester.cs:23`), so no application, machine, or user identity is populated by any caller in this repository.

## Call pattern

Each remote member follows the same shape: compose a relative URL, call one `JsonHttpClient` method, and unwrap the result.

| `JsonHttpClient` member | Returns | Used by this client for |
| --- | --- | --- |
| `GetResult<T>(url)` | `T`, returned directly with no unwrapping | `Track(long)` only |
| `GetResults<T>(url)` | `T[]` | Not used by this client |
| `PostJson<T>(url, body)` | Result envelope with `.Results` | `Convert` (both overloads), `Merge`, `IsPdfa` — each unwraps with `.Results.FirstOrDefault()` |

An empty `.Results` collection from `PostJson<T>` silently yields `default(T)` — `false` for `IsPdfa`, `null` for `Convert`/`Merge` — with no exception raised. See [Tech debt](tech-debt.md).

## Design constraints
- No public constructor or member accepts a `CancellationToken`; every remote call is
  fully synchronous and blocks the calling thread for the HTTP round trip.
- The single constructor makes `_jsonHttpClient` a `readonly` field assigned once, so an instance
  is immutable and has no shared mutable state after construction.
- The vestigial `private readonly string _pdfRootDirectory;` field (`PdfClient.cs:13`) is declared
  but never assigned or read.

## Inherited framework behavior

Serialization, request composition, timeout enforcement, and the `PostJson`/`GetResult` result envelope are implemented inside `SAIF.Web.API.Clients.JsonHttpClient` and are not re-documented here.

## Related documentation
- [Client index](index.md)
- [Overview](overview.md)
- [Package](package.md)
- [Client reference](reference.md)
- [Endpoint map](endpoint-map.md)
- [Consumption](consumption.md)
- [Contracts](contracts.md)
- [Dependencies](dependencies.md)
- [Business rules](business-rules.md)
- [Tech debt](tech-debt.md)

## Sources
- `PdfAPI.Client\PdfClient.cs`
- `PdfAPI.Client\IPdfClient.cs`
- `PdfAPI.Core\Services\PdfClientService.cs`
